var lang = {
  en: {
    required: 'This field is required',
    email: 'This field requires a valid e-mail address',
    number: 'This field requires a number',
    integer: 'This field requires an integer value',
    url: 'This field requires a valid website URL',
    tel: 'This field requires a valid telephone number',
    maxlength: 'This fields length must be < ${1}',
    minlength: 'This fields length must be > ${1}',
    min: 'Minimum value for this field is ${1}',
    max: 'Maximum value for this field is ${1}',
    pattern: 'Please match the requested format',
    equals: 'The two fields do not match',
  },
}

function findAncestor(el, cls) {
  while ((el = el.parentElement) && !el.classList.contains(cls)) {}
  return el
}

function tmpl(o) {
  var _arguments = arguments

  return this.replace(/\${([^{}]*)}/g, function (a, b) {
    return _arguments[b]
  })
}

function groupedElemCount(input) {
  return input.pristine.self.form.querySelectorAll(
    'input[name="' + input.getAttribute('name') + '"]:checked'
  ).length
}

function mergeConfig(obj1, obj2) {
  for (var attr in obj2) {
    if (!(attr in obj1)) {
      obj1[attr] = obj2[attr]
    }
  }
  return obj1
}

var defaultConfig = {
  classTo: 'form-group',
  errorClass: 'has-danger',
  successClass: 'has-success',
  errorTextParent: 'form-group',
  errorTextTag: 'div',
  errorTextClass: 'text-help',
}

var PRISTINE_ERROR = 'pristine-error'
var SELECTOR =
  'input:not([type^=hidden]):not([type^=submit]):not([type^=button]):not(.skip-validation), select, textarea'
var ALLOWED_ATTRIBUTES = [
  'required',
  'min',
  'max',
  'minlength',
  'maxlength',
  'pattern',
]
var EMAIL_REGEX =
  /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

var MESSAGE_REGEX = /-message(?:-([a-z]{2}(?:_[A-Z]{2})?))?/ // matches, -message, -message-en, -message-en_US
var currentLocale = 'en'
var validators = {}

var _ = function _(name, validator) {
  validator.name = name
  if (validator.priority === undefined) validator.priority = 1
  validators[name] = validator
}

_('text', {
  fn: function fn(val) {
    return true
  },
  priority: 0,
})
_('required', {
  fn: function fn(val) {
    return this.type === 'radio' || this.type === 'checkbox'
      ? groupedElemCount(this)
      : val !== undefined && val !== ''
  },
  priority: 99,
  halt: true,
})
_('email', {
  fn: function fn(val) {
    return !val || EMAIL_REGEX.test(val)
  },
})
_('number', {
  fn: function fn(val) {
    return !val || !isNaN(parseFloat(val))
  },
  priority: 2,
})
_('integer', {
  fn: function fn(val) {
    return !val || /^\d+$/.test(val)
  },
})
_('minlength', {
  fn: function fn(val, length) {
    return !val || val.length >= parseInt(length)
  },
})
_('maxlength', {
  fn: function fn(val, length) {
    return !val || val.length <= parseInt(length)
  },
})
_('min', {
  fn: function fn(val, limit) {
    return (
      !val ||
      (this.type === 'checkbox'
        ? groupedElemCount(this) >= parseInt(limit)
        : parseFloat(val) >= parseFloat(limit))
    )
  },
})
_('max', {
  fn: function fn(val, limit) {
    return (
      !val ||
      (this.type === 'checkbox'
        ? groupedElemCount(this) <= parseInt(limit)
        : parseFloat(val) <= parseFloat(limit))
    )
  },
})
_('pattern', {
  fn: function fn(val, pattern) {
    var m = pattern.match(new RegExp('^/(.*?)/([gimy]*)$'))
    return !val || new RegExp(m[1], m[2]).test(val)
  },
})
_('equals', {
  fn: function fn(val, otherFieldSelector) {
    var other = document.querySelector(otherFieldSelector)
    return other && ((!val && !other.value) || other.value === val)
  },
})

function Pristine(form, config, live) {
  var self = this

  init(form, config, live)

  function init(form, config, live) {
    form.setAttribute('novalidate', 'true')

    self.form = form
    self.config = mergeConfig(config || {}, defaultConfig)
    self.live = !(live === false)
    self.fields = Array.from(form.querySelectorAll(SELECTOR)).map(
      function (input) {
        var fns = []
        var params = {}
        var messages = {}

        ;[].forEach.call(input.attributes, function (attr) {
          if (/^data-pristine-/.test(attr.name)) {
            var name = attr.name.substr(14)
            var messageMatch = name.match(MESSAGE_REGEX)
            if (messageMatch !== null) {
              var locale =
                messageMatch[1] === undefined ? 'en' : messageMatch[1]
              if (!messages.hasOwnProperty(locale)) messages[locale] = {}
              messages[locale][
                name.slice(0, name.length - messageMatch[0].length)
              ] = attr.value
              return
            }
            if (name === 'type') name = attr.value
            _addValidatorToField(fns, params, name, attr.value)
          } else if (~ALLOWED_ATTRIBUTES.indexOf(attr.name)) {
            _addValidatorToField(fns, params, attr.name, attr.value)
          } else if (attr.name === 'type') {
            _addValidatorToField(fns, params, attr.value)
          }
        })

        fns.sort(function (a, b) {
          return b.priority - a.priority
        })

        self.live &&
          input.addEventListener(
            !~['radio', 'checkbox'].indexOf(input.getAttribute('type'))
              ? 'input'
              : 'change',
            function (e) {
              self.validate(e.target)
            }.bind(self)
          )

        return (input.pristine = {
          input: input,
          validators: fns,
          params: params,
          messages: messages,
          self: self,
        })
      }.bind(self)
    )
  }

  function _addValidatorToField(fns, params, name, value) {
    var validator = validators[name]
    if (validator) {
      fns.push(validator)
      if (value) {
        var valueParams = name === 'pattern' ? [value] : value.split(',')
        valueParams.unshift(null) // placeholder for input's value
        params[name] = valueParams
      }
    }
  }

  /***
   * Checks whether the form/input elements are valid
   * @param input => input element(s) or a jquery selector, null for full form validation
   * @param silent => do not show error messages, just return true/false
   * @returns {boolean} return true when valid false otherwise
   */
  self.validate = function (input, silent) {
    silent = (input && silent === true) || input === true
    var fields = self.fields
    if (input !== true && input !== false) {
      if (input instanceof HTMLElement) {
        fields = [input.pristine]
      } else if (
        input instanceof NodeList ||
        input instanceof (window.$ || Array) ||
        input instanceof Array
      ) {
        fields = Array.from(input).map(function (el) {
          return el.pristine
        })
      }
    }

    var valid = true

    for (var i = 0; fields[i]; i++) {
      var field = fields[i]
      if (_validateField(field)) {
        !silent && _showSuccess(field)
      } else {
        valid = false
        !silent && _showError(field)
      }
    }
    return valid
  }

  /***
   * Get errors of a specific field or the whole form
   * @param input
   * @returns {Array|*}
   */
  self.getErrors = function (input) {
    if (!input) {
      var erroneousFields = []
      for (var i = 0; i < self.fields.length; i++) {
        var field = self.fields[i]
        if (field.errors.length) {
          erroneousFields.push({ input: field.input, errors: field.errors })
        }
      }
      return erroneousFields
    }
    if (input.tagName && input.tagName.toLowerCase() === 'select') {
      return input.pristine.errors
    }
    return input.length ? input[0].pristine.errors : input.pristine.errors
  }

  /***
   * Validates a single field, all validator functions are called and error messages are generated
   * when a validator fails
   * @param field
   * @returns {boolean}
   * @private
   */
  function _validateField(field) {
    var errors = []
    var valid = true
    for (var i = 0; field.validators[i]; i++) {
      var validator = field.validators[i]
      var params = field.params[validator.name]
        ? field.params[validator.name]
        : []
      params[0] = field.input.value
      if (!validator.fn.apply(field.input, params)) {
        valid = false

        if (typeof validator.msg === 'function') {
          errors.push(validator.msg(field.input.value, params))
        } else if (typeof validator.msg === 'string') {
          errors.push(tmpl.apply(validator.msg, params))
        } else if (
          validator.msg === Object(validator.msg) &&
          validator.msg[currentLocale]
        ) {
          // typeof generates unnecessary babel code
          errors.push(tmpl.apply(validator.msg[currentLocale], params))
        } else if (
          field.messages[currentLocale] &&
          field.messages[currentLocale][validator.name]
        ) {
          errors.push(
            tmpl.apply(field.messages[currentLocale][validator.name], params)
          )
        } else if (lang[currentLocale] && lang[currentLocale][validator.name]) {
          errors.push(tmpl.apply(lang[currentLocale][validator.name], params))
        }

        if (validator.halt === true) {
          break
        }
      }
    }
    field.errors = errors
    return valid
  }

  /***
   * Add a validator to a specific dom element in a form
   * @param elem => The dom element where the validator is applied to
   * @param fn => validator function
   * @param msg => message to show when validation fails. Supports templating. ${0} for the input's value, ${1} and
   * so on are for the attribute values
   * @param priority => priority of the validator function, higher valued function gets called first.
   * @param halt => whether validation should stop for this field after current validation function
   */
  self.addValidator = function (elem, fn, msg, priority, halt) {
    if (elem instanceof HTMLElement) {
      elem.pristine.validators.push({
        fn: fn,
        msg: msg,
        priority: priority,
        halt: halt,
      })
      elem.pristine.validators.sort(function (a, b) {
        return b.priority - a.priority
      })
    } else {
      console.warn('The parameter elem must be a dom element')
    }
  }

  /***
   * An utility function that returns a 2-element array, first one is the element where error/success class is
   * applied. 2nd one is the element where error message is displayed. 2nd element is created if doesn't exist and cached.
   * @param field
   * @returns {*}
   * @private
   */
  function _getErrorElements(field) {
    if (field.errorElements) {
      return field.errorElements
    }
    var errorClassElement = findAncestor(field.input, self.config.classTo)
    var errorTextParent = null,
      errorTextElement = null
    if (self.config.classTo === self.config.errorTextParent) {
      errorTextParent = errorClassElement
    } else {
      errorTextParent = errorClassElement.querySelector(
        '.' + self.config.errorTextParent
      )
    }
    if (errorTextParent) {
      errorTextElement = errorTextParent.querySelector('.' + PRISTINE_ERROR)
      if (!errorTextElement) {
        errorTextElement = document.createElement(self.config.errorTextTag)
        errorTextElement.className =
          PRISTINE_ERROR + ' ' + self.config.errorTextClass
        errorTextParent.appendChild(errorTextElement)
        errorTextElement.pristineDisplay = errorTextElement.style.display
      }
    }
    return (field.errorElements = [errorClassElement, errorTextElement])
  }

  function _showError(field) {
    var errorElements = _getErrorElements(field)
    var errorClassElement = errorElements[0],
      errorTextElement = errorElements[1]

    if (errorClassElement) {
      errorClassElement.classList.remove(self.config.successClass)
      errorClassElement.classList.add(self.config.errorClass)
    }
    if (errorTextElement) {
      errorTextElement.innerHTML = field.errors.join('<br/>')
      errorTextElement.style.display = errorTextElement.pristineDisplay || ''
    }
  }

  /***
   * Adds error to a specific field
   * @param input
   * @param error
   */
  self.addError = function (input, error) {
    input = input.length ? input[0] : input
    input.pristine.errors.push(error)
    _showError(input.pristine)
  }

  function _removeError(field) {
    var errorElements = _getErrorElements(field)
    var errorClassElement = errorElements[0],
      errorTextElement = errorElements[1]
    if (errorClassElement) {
      // IE > 9 doesn't support multiple class removal
      errorClassElement.classList.remove(self.config.errorClass)
      errorClassElement.classList.remove(self.config.successClass)
    }
    if (errorTextElement) {
      errorTextElement.innerHTML = ''
      errorTextElement.style.display = 'none'
    }
    return errorElements
  }

  function _showSuccess(field) {
    var errorClassElement = _removeError(field)[0]
    errorClassElement &&
      errorClassElement.classList.add(self.config.successClass)
  }

  /***
   * Resets the errors
   */
  self.reset = function () {
    for (var i = 0; self.fields[i]; i++) {
      self.fields[i].errorElements = null
    }
    Array.from(self.form.querySelectorAll('.' + PRISTINE_ERROR)).map(function (
      elem
    ) {
      elem.parentNode.removeChild(elem)
    })
    Array.from(self.form.querySelectorAll('.' + self.config.classTo)).map(
      function (elem) {
        elem.classList.remove(self.config.successClass)
        elem.classList.remove(self.config.errorClass)
      }
    )
  }

  /***
   * Resets the errors and deletes all pristine fields
   */
  self.destroy = function () {
    self.reset()
    self.fields.forEach(function (field) {
      delete field.input.pristine
    })
    self.fields = []
  }

  self.setGlobalConfig = function (config) {
    defaultConfig = config
  }

  return self
}

/***
 *
 * @param name => Name of the global validator
 * @param fn => validator function
 * @param msg => message to show when validation fails. Supports templating. ${0} for the input's value, ${1} and
 * so on are for the attribute values
 * @param priority => priority of the validator function, higher valued function gets called first.
 * @param halt => whether validation should stop for this field after current validation function
 */
Pristine.addValidator = function (name, fn, msg, priority, halt) {
  _(name, { fn: fn, msg: msg, priority: priority, halt: halt })
}

Pristine.addMessages = function (locale, messages) {
  var langObj = lang.hasOwnProperty(locale) ? lang[locale] : (lang[locale] = {})

  Object.keys(messages).forEach(function (key, index) {
    langObj[key] = messages[key]
  })
}

Pristine.setLocale = function (locale) {
  currentLocale = locale
}

export default Pristine
